"""
MOB: Mapped Objects

Implements objects that map their value to specific locations in an 
address space. Address spaces can be anything that can be addressed for 
reading and writing, such as in-memory space, file space, as well as many
interesting ideas (shared-memory space to share objects between processes,
or an RPC-space to share objects between different machines).

* MappedValue is a proxy-like object that "references" the mapped object in the
space.
* Codecs are like types: they define how the value is [de]serialized to the
space
* Ref references mapped-values of some codec ("type"). Ref(Int()) is like
(long *) in C. You can reference objects only in the same space!
"""
import decimal as _decimal
from weakref import WeakValueDictionary
from struct import Struct
from array import array


#===============================================================================
# Address spaces
#===============================================================================
class AddressSpace(object):
    def read(self, index, count):
        raise NotImplementedError
    def write(self, index, data):
        pass

class MemorySpace(AddressSpace):
    __slots__ = ["arr"]
    def __init__(self):
        self.arr = array("c")
    def read(self, index, count):
        data = self.arr[index:index+count].tostring()
        if len(data) < count:
            raise EOFError
        return data
    def write(self, index, data):
        padding = index + len(data) - len(self.arr)
        if padding > 0:
            self.arr.extend("\x00" * padding)
        self.arr[index:index+len(data)] = array("c", data)

class FileSpace(AddressSpace):
    __slots__ = ["file"]
    def __init__(self, file):
        self.file = file
    def read(self, index, count):
        self.file.seek(index)
        data = self.file.read(count)
        if len(data) < count:
            raise EOFError
        return data
    def write(self, index, data):
        self.file.seek(index)
        self.file.write(data)


#===============================================================================
# codecs
#===============================================================================
class Codec(object):
    def sizeof(self):
        raise NotImplementedError
    def encode(self, value, space):
        raise NotImplementedError
    def decode(self, data, space):
        raise NotImplementedError


#===============================================================================
# value codecs
#===============================================================================
class Int(Codec):
    packers = {
        8  : Struct("<b"), 
        16 : Struct("<h"),
        32 : Struct("<l"),
        64 : Struct("<q"),
    }
    def __init__(self, bits = 32):
        self.bits = bits
        self.size = bits // 8
        self.packer = self.packers[bits]
    def __repr__(self):
        return "Int(%r)" % (self.bits,)
    def sizeof(self):
        return self.size
    def encode(self, value, space):
        return self.packer.pack(value)
    def decode(self, data, space):
        return self.packer.unpack(data)[0]

class Float(Codec):
    packer = Struct("<d")
    def __init__(self):
        self.size = bits // 8
    def __repr__(self):
        return "Float()"
    def sizeof(self):
        return self.packer.size
    def encode(self, value, space):
        return self.packer.pack(value)
    def decode(self, data, space):
        return self.packer.unpack(data)[0]

class Long(Codec):
    def __init__(self, digits = 40):
        self.digits = digits
    def __repr__(self):
        return "Long(%r)" % (self.digits,)
    def sizeof(self):
        return self.digits
    def encode(self, value, space):
        d = self.digits
        return str(value).zfill(d)[:d]
    def decode(self, data, space):
        return long(data)

class String(Codec):
    def __init__(self, size = 32):
        self.size = size
    def __repr__(self):
        return "String(%r)" % (self.size,)
    def sizeof(self):
        return self.size
    def encode(self, value, space):
        v = value[:self.size]
        return v + "\x00" * (self.size - len(v))
    def decode(self, data, space):
        return data.rstrip("\x00")

class Decimal(Codec):
    def __init__(self, digits = 40):
        self.context = _decimal.Context(prec = digits)
        self.packer = String(digits + 1)
        self.digits = digits
    def __repr__(self):
        return "Decimal(%r)" % (self.digits,)
    def sizeof(self):
        return self.packer.sizeof()
    def encode(self, value, space):
        return self.packer.encode(str(value.normalize(self.context)), space)
    def decode(self, data, space):
        return _decimal.Decimal(self.packer.decode(data, space), context = self.context)


#===============================================================================
# compound value codecs
#===============================================================================
class Array(Codec):
    def __init__(self, elemcodec, count):
        assert count > 0
        self.elemcodec = elemcodec
        self.elemsize = elemcodec.sizeof()
        self.count = count
    def __repr__(self):
        return "Array(%r, %r)" % (self.elemcodec, self.count)
    def sizeof(self):
        return self.count * self.elemsize
    def __len__(self):
        return self.count
    def encode(self, value, space):
        subencode = self.elemcodec.encode
        return "".join(subencode(item, space) for item in value)
    def decode(self, data, space):
        value = []
        elemsize = self.elemsize
        subdecode = self.elemcodec.decode
        value = [subdecode(data[i:i+elemsize], space)
            for i in xrange(0, elemsize * count, elemsize)]
        return value
    def offsetof(self, index):
        if index < 0 or index > self.count:
            raise IndexError(index)
        return index * self.elemsize, self.elemcodec
    def keysof(self):
        return xrange(0, self.count)

class Record(Codec):
    def __init__(self, **fields):
        offset = 0
        self.fields = {}
        for name, codec in sorted(fields.items()):
            self.fields[name] = (offset, codec)
            offset += codec.sizeof()
        self.size = offset
    def __repr__(self):
        kwargs = ("%s = %r" % (k, v[1]) for k, v in self.fields.iteritems())
        return "Record(%s)" % (", ".join(sorted(kwargs)),)
    def sizeof(self):
        return self.size
    def encode(self, value, space):
        data = array("c", "\x00" * self.size)
        for name, (offset, subcodec) in self.fields.iteritems():
            subdata = subcodec.encode(value[name], space)
            data[offset:offset+subcodec.sizeof()] = array("c", subdata)
        return data.tostring()
    def decode(self, data, space):
        value = {}
        for name, (offset, subcodec) in self.fields.iteritems():
            subdata = data[offset:offset+subcodec.sizeof()]
            value[name] = subcodec.decode(subdata, space)
        return value
    def __len__(self):
        return len(self.fields)
    def keysof(self):
        return self.fields.iterkeys()
    def offsetof(self, key):
        return self.fields[key]


#===============================================================================
# reference codecs
#===============================================================================
class Ref(Codec):
    packer = Int(64)
    NULL = 0
    def __init__(self, codec):
        self.codec = codec
    def __repr__(self):
        return "Ref(%r)" % (self.codec,)
    def sizeof(self):
        return self.packer.sizeof()
    def encode(self, value, space):
        if value is None:
            return self.packer.encode(self.NULL, space)
        if not isinstance(value, MappedValue):
            raise TypeError("can only refer to MappedObject instances")
        if value._space is not space:
            raise ValueError("object is in a different space")
        return self.packer.encode(value._pos, space)
    def decode(self, data, space):
        pos = self.packer.decode(data, space)
        if pos == self.NULL:
            return None
        return MappedValue(self.codec, space, pos)


#===============================================================================
# mapped value
#===============================================================================
class MappedValue(object):
    __slots__ = ["_codec", "_space", "_pos", "_size", "_data", "_value", 
        "__weakref__"]
    def __init__(self, codec, space, pos):
        object.__setattr__(self, "_codec", codec)
        object.__setattr__(self, "_space", space)
        object.__setattr__(self, "_pos", pos)
        object.__setattr__(self, "_data", NotImplemented)
        object.__setattr__(self, "_value", NotImplemented)
        object.__setattr__(self, "_size", codec.sizeof())
    
    def __del__(self):
        self.flush()
    
    def __new__(cls, *args):
        if not hasattr(cls, "_cache_"):
            cls._cache_ = WeakValueDictionary()
        if args in cls._cache_:
            inst = cls._cache_[args]
        else:
            inst = object.__new__(cls, *args)
            cls._cache_[args] = inst
        return inst
    
    def __repr__(self):
        return "MappedValue(%r, %r, %r)" % (self._codec, self._space, self._pos)
    
    def sizeof(self):
        return self._size
    
    def get(self):
        if self._value is not NotImplemented:
            return self._value
        data = self._space.read(self._pos, self._size)
        value = self._codec.decode(data, self._space)
        object.__setattr__(self, "_data", data)
        object.__setattr__(self, "_value", value)
        return value
    
    def set(self, value):
        object.__setattr__(self, "_value", value)
        object.__setattr__(self, "_data", None)
    
    def flush(self):
        if self._value is NotImplemented:
            return
        if self._data is not NotImplemented:
            data = self._codec.encode(self._value, self._space)
            object.__setattr__(self, "_data", data)
        self._space.write(self._pos, self._data)
    
    def __len__(self):
        return len(self._codec)
    
    def __iter__(self):
        for k in self._codec.keysof():
            yield self[k]
    
    def __getitem__(self, index):
        offset, subcodec = self._codec.offsetof(index)
        return self.__class__(subcodec, self._space, self._pos + offset)
    
    def __setitem__(self, index, value):
        self[index].set(value)
    
    __getattr__ = __getitem__
    __setattr__ = __setitem__


#===============================================================================
# API
#===============================================================================
def bind(codec, space):
    """'binds' a codec to a space, returning a mapped object"""
    return MappedValue(codec, space, 0)


if __name__ == "__main__":
    #from cStringIO import StringIO
    #s = FileSpace(StringIO())
    s = MemorySpace()
    
    #intarr = Array(Int(), 100)
    #m = MappedValue(intarr, s, 0)
    #print m
    #print m[300]
    
    person_rec = Record(firstname = String(), lastname = String(), id = Int(64))
    grades_rec = Record(person = Ref(person_rec), math_grade = Int(), english_grade = Int())
    persons_table = Array(person_rec, 1000)
    grades_table = Array(grades_rec, 1000)
    database = Record(persons = persons_table, grades = grades_table)
    
    m = bind(database, s)
    m.persons[3] = {"firstname" : "moshe", "lastname" : "baavur", "id" : 1337}
#    print m.persons[3].firstname.get()
#    print m.persons[3].get()
    
    m.grades[78].person = m.persons[3]
    m.grades[78].math_grade = 81
    m.grades[78].english_grade = 99
    print m.grades[78].get()
    print
    print m.grades[78].person
    print
    print m.grades[78].person.get()
    print
    print m.grades[78].person.get().get()
    
    #print m.grades[780].person










































